(ns packet.procedure
  (:require [clojure.zip :as z]
            [packet.core :as p]
            [packet.utils :as u]))

#_"
一个过程就是一个有限状态机，调用这个过程即进入初始状态。客户不用等待其结果。
在一个状态下会发出一些报文，然后等待接收报文，根据接收的报文内容进入其他状态
发出报文时需要提供一些值，状态转换时也会对外提供一些值。
状态机对外像一个提供数据的流，可能会终结。
"



(defn matches
  "判断一个值value是否满足target的形状。
  对target中的符号，返回对应的符号值映射，如果不满足则返回false"
  [value target]
  (cond (and (map? target) (map? value))
        (reduce-kv (fn [r k v]
                     (let [r2 (and (contains? value k)
                                   (matches (get value k)
                                            (if (= v '?)
                                              (symbol k)
                                              v)))]
                       (if (false? r2)
                         (reduced false)
                         (merge r r2))))
                   nil
                   target)
        
        (and (sequential? target) (sequential? value))
        (let [rs (map matches value target)]
          (when (not-any? false? rs)
            (apply merge rs)))
        
        (symbol? target) {target value}
        (= value target) nil
        :else false))

(defn replace-by
  "将value中的符号用db中对应的值替代"
  [value db]
  (cond (map? value)
        (reduce-kv (fn [m k v]
                     (assoc m k (replace-by (if (= v '?)
                                              (symbol k)
                                              v)
                                            db)))
                   nil
                   value)
        
        (sequential? value)
        (map #(replace-by % db) value)
        
        (symbol? value)
        (if (or (contains? db value)
                (contains? db (keyword value)))
          (or (get db value) (get db (keyword value)))
          (throw (ex-info "信息缺失" {:arg value})))
        
        :else value))

;;定义状态信息有什么，进入这个状态会发送什么，接收到什么信息怎么处理（进入什么状态，携带什么信息）
;;发送信息有send和send-if两种表示，后者带一个条件
;;动作有
;;(exit data)
;;(goto next-state & args)
;;recv-next 继续接收下一个报文(不再发送报文)
;;(f & args) 一个以当前状态为参数的函数，重新进入当前状态

(defn make-data
  [args]
  (if (->> (partition 2 args)
           (map first)
           (every? keyword?))
    (let [{:as m} args] m)
    (let [[act & args] args]
      `(fn [d#] (~act d# ~@(map #(quote %) args))))))

(defn parse-action
  [action]
  (if-let [[act & args] action]
    (case act
      exit {:next-state ::exit
            :data (let [{:as m} args] m)}
      goto {:next-state (keyword (first args))
            :data (vec (rest args))}
      recv-next {:next-state ::this
                 :data (make-data args)}
      {:next-state ::this
       :data (vec args)})
    {:next-state ::exit}))

(declare run)
(defn exec-action
  [action ctx state-maker sender recver]
  (let [{:keys [next-state data]} action
        data (if (map? data)
               (replace-by data ctx)
               (data ctx))]
    (case next-state
      nil nil
      ::exit data
      ::this (run state-maker sender recver ctx data false)
      (when-let [f (resolve (symbol next-state))]
        (run @f sender recver ctx data true)))))

(defmacro defstate
  "定义一个函数，传入上一个状态的数据和参数，返回新状态"
  [name & body]
  (let [[doc body] (if (string? (first body))
                     [(first body) (rest body)]
                     [nil body])
        args (mapv keyword (first body))
        body (rest body)
        m (reduce (fn [m l]
                    (case (first l)
                      send (assoc m :sending {:data (second l)})
                      send-if (assoc m :sending {:cond (second l)
                                                 :data (nth l 2)})
                      timeout (let [[_ t retry n & args] l]
                                (if (= retry 'retry)
                                  (assoc m :timeout {:time t
                                                     :retry n
                                                     :act (parse-action args)})))))
                  nil
                  (take-while list? body))
        body (drop-while list? body)]
    `(defn ^{:doc ~doc} ~name [ctx# init-args#]
       (let [ls# (merge ctx# (zipmap ~args init-args#))]
         {:local-status ls#
          :sending (update ~(:sending m) :data replace-by ls#)
          :targets ~(mapv (fn [[condition action]]
                            (when-not (map? condition)
                              (u/line-error "条件必须是映射" condition))
                            (let [action (cond-> action (not (list? action)) list)]
                              [`'~(u/keyword-map condition)
                               (parse-action action)]))
                          (partition 2 body))}))))

(def ^:dynamic *sender*)
(def ^:dynamic *recver*)

(defn run
  "运行有限状态机，惰性返回各状态的接收到的数据"
  [state-maker sender recver ctx args send?]
  (lazy-seq
   (let [state (state-maker ctx args)
         {:keys [time retry act]} (:timeout state)
         {:keys [local-status sending-data targets]} state
         r (loop [n 0]
             (when (= n (or retry 1))
               (exec-action act local-status
                            state-maker sender recver))
             (when (and send? sending-data) (sender sending-data))
             (if-let [r (recver (or time 5))]
               r
               (recur (inc n))))]
     (cons r
           (let [[action m] (some (fn [[condition action]]
                                    (let [m (matches (:obj r) condition)]
                                      (when-not (false? m)
                                        [action m])))
                                  targets)]
             (exec-action action (merge local-status m)
                          state-maker sender recver))))))

(defmacro defprocedure
  "定义一个过程
  name是返回的函数名，返回的函数以初始状态值为参数，返回接收的报文解析结果惰性序列
  protocol是规约名，init-state是初始状态，
  send是发送函数，参数是发送的字节序列
  recv是接收函数，参数是超时值，返回字节序列，超时时返回nil"
  [name protocol init-state send recv]
  `(let [sender# (fn [obj#] (~send (:data (p/build ~protocol obj#))))
         recver# (fn [t#] (when-let [r# (~recv t#)]
                            (p/parse ~protocol r#)))]
     (defn ~name [& args#]
       (run ~init-state sender# recver# nil args# true))))


;;状态信息自动传递给下一个状态
(defstate init
  "读取有效电表数"
  [addr]
  (send {})
  (timeout 10 retry 3)
  {err 1} (exit :err 1)
  {count ?} (goto read-meter-params 0 1))

(defstate read-meter-params
  "读电表参数"
  [found current]
  (send #_send-if #_(and (< found count) (< current (* count 1.5)))
        {
         })
  (timeout 5 retry 3 update current inc)
  {baud ?} (goto read-load))

